/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.simonvt.calendarview;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

import android.annotation.TargetApi;
import android.app.Service;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

/**
 * This class is a calendar widget for displaying and selecting dates. The range
 * of dates supported by this calendar is configurable. A user can select a date
 * by taping on it and can scroll and fling the calendar to a desired date.
 * 
 * @attr ref android.R.styleable#CalendarView_showWeekNumber
 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
 * @attr ref android.R.styleable#CalendarView_minDate
 * @attr ref android.R.styleable#CalendarView_maxDate
 * @attr ref android.R.styleable#CalendarView_shownWeekCount
 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
 * @attr ref android.R.styleable#CalendarView_weekNumberColor
 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
 */
// @Widget
public class CalendarView extends FrameLayout {

	/**
	 * Tag for logging.
	 */
	private static final String LOG_TAG = CalendarView.class.getSimpleName();

	/**
	 * Default value whether to show week number.
	 */
	private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;

	/**
	 * The number of milliseconds in a day.e
	 */
	private static final long MILLIS_IN_DAY = 86400000L;

	/**
	 * The number of day in a week.
	 */
	private static final int DAYS_PER_WEEK = 7;

	/**
	 * The number of milliseconds in a week.
	 */
	private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;

	/**
	 * Affects when the month selection will change while scrolling upe
	 */
	private static final int SCROLL_HYST_WEEKS = 2;

	/**
	 * How long the GoTo fling animation should last.
	 */
	private static final int GOTO_SCROLL_DURATION = 1000;

	/**
	 * The duration of the adjustment upon a user scroll in milliseconds.
	 */
	private static final int ADJUSTMENT_SCROLL_DURATION = 500;

	/**
	 * How long to wait after receiving an onScrollStateChanged notification
	 * before acting on it.
	 */
	private static final int SCROLL_CHANGE_DELAY = 40;

	/**
	 * String for parsing dates.
	 */
	private static final String DATE_FORMAT = "MM/dd/yyyy";

	/**
	 * The default minimal date.
	 */
	private static final String DEFAULT_MIN_DATE = "01/01/1900";

	/**
	 * The default maximal date.
	 */
	private static final String DEFAULT_MAX_DATE = "01/01/2100";

	private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;

	private static final int DEFAULT_DATE_TEXT_SIZE = 14;

	private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;

	private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;

	private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;

	private static final int UNSCALED_BOTTOM_BUFFER = 20;

	private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;

	private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;

	private final int mWeekSeperatorLineWidth;

	private int mDateTextSize;

	private Drawable mSelectedDateVerticalBar;

	private final int mSelectedDateVerticalBarWidth;

	private int mSelectedWeekBackgroundColor;

	private int mFocusedMonthDateColor;

	private int mUnfocusedMonthDateColor;

	private int mWeekSeparatorLineColor;

	private int mWeekNumberColor;

	private int mBoderColor;

	private int mDaySeparatorLineColor;
	
	private int mSelectedDayBackgroundFillColor;
	
	private int mSelectedDayBackgroundBoderColor;

	private int mWeekDayTextAppearanceResId;

	private int mDateTextAppearanceResId;

	/**
	 * The top offset of the weeks list.
	 */
	private int mListScrollTopOffset = 2;

	/**
	 * The visible height of a week view.
	 */
	private int mWeekMinVisibleHeight = 12;

	/**
	 * The visible height of a week view.
	 */
	private int mBottomBuffer = 20;

	/**
	 * The number of shown weeks.
	 */
	private int mShownWeekCount;

	/**
	 * Flag whether to show the week number.
	 */
	private boolean mShowWeekNumber;

	/**
	 * The number of day per week to be shown.
	 */
	private int mDaysPerWeek = 7;

	/**
	 * The friction of the week list while flinging.
	 */
	private float mFriction = .05f;

	/**
	 * Scale for adjusting velocity of the week list while flinging.
	 */
	private float mVelocityScale = 0.333f;

	/**
	 * The adapter for the weeks list.
	 */
	private WeeksAdapter mAdapter;

	/**
	 * The weeks list.
	 */
	private ListView mListView;

	/**
	 * The name of the month to display.
	 */
	private TextView mMonthName;

	/**
	 * The header with week day names.
	 */
	private ViewGroup mDayNamesHeader;

	/**
	 * Cached labels for the week names header.
	 */
	private String[] mDayLabels;

	/**
	 * The first day of the week.
	 */
	private int mFirstDayOfWeek;

	/**
	 * Which month should be displayed/highlighted [0-11].
	 */
	private int mCurrentMonthDisplayed;

	/**
	 * Used for tracking during a scroll.
	 */
	private long mPreviousScrollPosition;

	/**
	 * Used for tracking which direction the view is scrolling.
	 */
	private boolean mIsScrollingUp = false;

	/**
	 * The previous scroll state of the weeks ListView.
	 */
	private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;

	/**
	 * The current scroll state of the weeks ListView.
	 */
	private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;

	/**
	 * Listener for changes in the selected day.
	 */
	private OnDateChangeListener mOnDateChangeListener;

	/**
	 * Command for adjusting the position after a scroll/fling.
	 */
	private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();

	/**
	 * Temporary instance to avoid multiple instantiations.
	 */
	private Calendar mTempDate;

	/**
	 * The first day of the focused month.
	 */
	private Calendar mFirstDayOfMonth;

	/**
	 * The start date of the range supported by this picker.
	 */
	private Calendar mMinDate;

	/**
	 * The end date of the range supported by this picker.
	 */
	private Calendar mMaxDate;

	/**
	 * Date format for parsing dates.
	 */
	private final java.text.DateFormat mDateFormat = new SimpleDateFormat(
			DATE_FORMAT);

	/**
	 * The current locale.
	 */
	private Locale mCurrentLocale;

	/**
	 * The callback used to indicate the user changes the date.
	 */
	public interface OnDateChangeListener {

		/**
		 * Called upon change of the selected day.
		 * 
		 * @param view
		 *            The view associated with this listener.
		 * @param year
		 *            The year that was set.
		 * @param month
		 *            The month that was set [0-11].
		 * @param dayOfMonth
		 *            The day of the month that was set.
		 */
		public void onSelectedDayChange(CalendarView view, int year, int month,
				int dayOfMonth);
	}

	public CalendarView(Context context) {
		this(context, null);
	}

	public CalendarView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public CalendarView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, 0);

		// initialization based on locale
		setCurrentLocale(Locale.getDefault());

		TypedArray attributesArray = context.obtainStyledAttributes(attrs,
				R.styleable.CalendarView, R.attr.calendarViewStyle, 0);
		mShowWeekNumber = attributesArray.getBoolean(
				R.styleable.CalendarView_cv_showWeekNumber,
				DEFAULT_SHOW_WEEK_NUMBER);
		mFirstDayOfWeek = attributesArray.getInt(
				R.styleable.CalendarView_cv_firstDayOfWeek,
				// LocaleData.get(Locale.getDefault()).firstDayOfWeek);
				Calendar.getInstance().getFirstDayOfWeek());
		String minDate = attributesArray
				.getString(R.styleable.CalendarView_cv_minDate);
		if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) {
			parseDate(DEFAULT_MIN_DATE, mMinDate);
		}
		String maxDate = attributesArray
				.getString(R.styleable.CalendarView_cv_maxDate);
		if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) {
			parseDate(DEFAULT_MAX_DATE, mMaxDate);
		}
		if (mMaxDate.before(mMinDate)) {
			throw new IllegalArgumentException(
					"Max date cannot be before min date.");
		}
		mShownWeekCount = attributesArray.getInt(
				R.styleable.CalendarView_cv_shownWeekCount,
				DEFAULT_SHOWN_WEEK_COUNT);
		mSelectedWeekBackgroundColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_selectedWeekBackgroundColor, 0);
		mFocusedMonthDateColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_focusedMonthDateColor, 0);
		mUnfocusedMonthDateColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_unfocusedMonthDateColor, 0);
		mWeekSeparatorLineColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_weekSeparatorLineColor, 0);
		mWeekNumberColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_weekNumberColor, 0);
		mSelectedDateVerticalBar = attributesArray
				.getDrawable(R.styleable.CalendarView_cv_selectedDateVerticalBar);

		mDateTextAppearanceResId = attributesArray.getResourceId(
				R.styleable.CalendarView_cv_dateTextAppearance,
				android.R.style.TextAppearance_Small);
		updateDateTextSize();

		mWeekDayTextAppearanceResId = attributesArray.getResourceId(
				R.styleable.CalendarView_cv_weekDayTextAppearance,
				DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);

		mBoderColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_boderColor, 0);
		mDaySeparatorLineColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_daySeparatorLineColor, 0);
		
		mSelectedDayBackgroundBoderColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_selectedDayBackgroundBoderColor, 0);
		mSelectedDayBackgroundFillColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_selectedDayBackgroundFillColor, 0);
		int mothNameColor = attributesArray.getColor(
				R.styleable.CalendarView_cv_monthNameColor, 0);
		
		final Drawable horizontalDivider = attributesArray
				.getDrawable(R.styleable.CalendarView_cv_dividerHorizontal);
		attributesArray.recycle();

		DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
		mWeekMinVisibleHeight = (int) TypedValue.applyDimension(
				TypedValue.COMPLEX_UNIT_DIP, UNSCALED_WEEK_MIN_VISIBLE_HEIGHT,
				displayMetrics);
		mListScrollTopOffset = (int) TypedValue.applyDimension(
				TypedValue.COMPLEX_UNIT_DIP, UNSCALED_LIST_SCROLL_TOP_OFFSET,
				displayMetrics);
		mBottomBuffer = (int) TypedValue.applyDimension(
				TypedValue.COMPLEX_UNIT_DIP, UNSCALED_BOTTOM_BUFFER,
				displayMetrics);
		mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(
				TypedValue.COMPLEX_UNIT_DIP,
				UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
		mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(
				TypedValue.COMPLEX_UNIT_DIP,
				UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);

		LayoutInflater layoutInflater = (LayoutInflater) context
				.getSystemService(Service.LAYOUT_INFLATER_SERVICE);
		View content = layoutInflater.inflate(R.layout.calendar_view, null,
				false);
		addView(content);

		mListView = (ListView) findViewById(android.R.id.list);
		mDayNamesHeader = (ViewGroup) content.findViewById(R.id.cv_day_names);
		mMonthName = (TextView) content.findViewById(R.id.cv_month_name);
		mMonthName.setTextColor(mothNameColor);
		((ImageView) findViewById(R.id.cv_divider))
				.setImageDrawable(horizontalDivider);

		setUpHeader();
		setUpListView();
		setUpAdapter();

		// go to today or whichever is close to today min or max date
		mTempDate.setTimeInMillis(System.currentTimeMillis());
		if (mTempDate.before(mMinDate)) {
			goTo(mMinDate, false, true, true);
		} else if (mMaxDate.before(mTempDate)) {
			goTo(mMaxDate, false, true, true);
		} else {
			goTo(mTempDate, false, true, true);
		}

		invalidate();
	}

	/**
	 * Sets the number of weeks to be shown.
	 * 
	 * @param count
	 *            The shown week count.
	 * 
	 * @attr ref android.R.styleable#CalendarView_shownWeekCount
	 */
	public void setShownWeekCount(int count) {
		if (mShownWeekCount != count) {
			mShownWeekCount = count;
			invalidate();
		}
	}

	/**
	 * Gets the number of weeks to be shown.
	 * 
	 * @return The shown week count.
	 * 
	 * @attr ref android.R.styleable#CalendarView_shownWeekCount
	 */
	public int getShownWeekCount() {
		return mShownWeekCount;
	}

	/**
	 * Sets the background color for the selected week.
	 * 
	 * @param color
	 *            The week background color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
	 */
	public void setSelectedWeekBackgroundColor(int color) {
		if (mSelectedWeekBackgroundColor != color) {
			mSelectedWeekBackgroundColor = color;
			final int childCount = mListView.getChildCount();
			for (int i = 0; i < childCount; i++) {
				WeekView weekView = (WeekView) mListView.getChildAt(i);
				if (weekView.mHasSelectedDay) {
					weekView.invalidate();
				}
			}
		}
	}

	/**
	 * Gets the background color for the selected week.
	 * 
	 * @return The week background color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
	 */
	public int getSelectedWeekBackgroundColor() {
		return mSelectedWeekBackgroundColor;
	}

	/**
	 * Sets the color for the dates of the focused month.
	 * 
	 * @param color
	 *            The focused month date color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
	 */
	public void setFocusedMonthDateColor(int color) {
		if (mFocusedMonthDateColor != color) {
			mFocusedMonthDateColor = color;
			final int childCount = mListView.getChildCount();
			for (int i = 0; i < childCount; i++) {
				WeekView weekView = (WeekView) mListView.getChildAt(i);
				if (weekView.mHasFocusedDay) {
					weekView.invalidate();
				}
			}
		}
	}

	/**
	 * Gets the color for the dates in the focused month.
	 * 
	 * @return The focused month date color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
	 */
	public int getFocusedMonthDateColor() {
		return mFocusedMonthDateColor;
	}

	/**
	 * Sets the color for the dates of a not focused month.
	 * 
	 * @param color
	 *            A not focused month date color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
	 */
	public void setUnfocusedMonthDateColor(int color) {
		if (mUnfocusedMonthDateColor != color) {
			mUnfocusedMonthDateColor = color;
			final int childCount = mListView.getChildCount();
			for (int i = 0; i < childCount; i++) {
				WeekView weekView = (WeekView) mListView.getChildAt(i);
				if (weekView.mHasUnfocusedDay) {
					weekView.invalidate();
				}
			}
		}
	}

	/**
	 * Gets the color for the dates in a not focused month.
	 * 
	 * @return A not focused month date color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
	 */
	public int getUnfocusedMonthDateColor() {
		return mFocusedMonthDateColor;
	}

	/**
	 * Sets the color for the week numbers.
	 * 
	 * @param color
	 *            The week number color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekNumberColor
	 */
	public void setWeekNumberColor(int color) {
		if (mWeekNumberColor != color) {
			mWeekNumberColor = color;
			if (mShowWeekNumber) {
				invalidateAllWeekViews();
			}
		}
	}

	/**
	 * Gets the color for the week numbers.
	 * 
	 * @return The week number color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekNumberColor
	 */
	public int getWeekNumberColor() {
		return mWeekNumberColor;
	}

	/**
	 * Sets the color for the separator line between weeks.
	 * 
	 * @param color
	 *            The week separator color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
	 */
	public void setWeekSeparatorLineColor(int color) {
		if (mWeekSeparatorLineColor != color) {
			mWeekSeparatorLineColor = color;
			invalidateAllWeekViews();
		}
	}

	/**
	 * Gets the color for the separator line between weeks.
	 * 
	 * @return The week separator color.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
	 */
	public int getWeekSeparatorLineColor() {
		return mWeekSeparatorLineColor;
	}

	/**
	 * Sets the drawable for the vertical bar shown at the beginning and at the
	 * end of the selected date.
	 * 
	 * @param resourceId
	 *            The vertical bar drawable resource id.
	 * 
	 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
	 */
	public void setSelectedDateVerticalBar(int resourceId) {
		Drawable drawable = getResources().getDrawable(resourceId);
		setSelectedDateVerticalBar(drawable);
	}

	/**
	 * Sets the drawable for the vertical bar shown at the beginning and at the
	 * end of the selected date.
	 * 
	 * @param drawable
	 *            The vertical bar drawable.
	 * 
	 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
	 */
	public void setSelectedDateVerticalBar(Drawable drawable) {
		if (mSelectedDateVerticalBar != drawable) {
			mSelectedDateVerticalBar = drawable;
			final int childCount = mListView.getChildCount();
			for (int i = 0; i < childCount; i++) {
				WeekView weekView = (WeekView) mListView.getChildAt(i);
				if (weekView.mHasSelectedDay) {
					weekView.invalidate();
				}
			}
		}
	}

	/**
	 * Gets the drawable for the vertical bar shown at the beginning and at the
	 * end of the selected date.
	 * 
	 * @return The vertical bar drawable.
	 */
	public Drawable getSelectedDateVerticalBar() {
		return mSelectedDateVerticalBar;
	}

	/**
	 * Sets the text appearance for the week day abbreviation of the calendar
	 * header.
	 * 
	 * @param resourceId
	 *            The text appearance resource id.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
	 */
	public void setWeekDayTextAppearance(int resourceId) {
		if (mWeekDayTextAppearanceResId != resourceId) {
			mWeekDayTextAppearanceResId = resourceId;
			setUpHeader();
		}
	}

	/**
	 * Gets the text appearance for the week day abbreviation of the calendar
	 * header.
	 * 
	 * @return The text appearance resource id.
	 * 
	 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
	 */
	public int getWeekDayTextAppearance() {
		return mWeekDayTextAppearanceResId;
	}

	/**
	 * Sets the text appearance for the calendar dates.
	 * 
	 * @param resourceId
	 *            The text appearance resource id.
	 * 
	 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
	 */
	public void setDateTextAppearance(int resourceId) {
		if (mDateTextAppearanceResId != resourceId) {
			mDateTextAppearanceResId = resourceId;
			updateDateTextSize();
			invalidateAllWeekViews();
		}
	}

	/**
	 * Gets the text appearance for the calendar dates.
	 * 
	 * @return The text appearance resource id.
	 * 
	 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
	 */
	public int getDateTextAppearance() {
		return mDateTextAppearanceResId;
	}

	@Override
	public void setEnabled(boolean enabled) {
		mListView.setEnabled(enabled);
	}

	@Override
	public boolean isEnabled() {
		return mListView.isEnabled();
	}

	@Override
	protected void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		setCurrentLocale(newConfig.locale);
	}

	@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
	@Override
	public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
		super.onInitializeAccessibilityEvent(event);
		event.setClassName(CalendarView.class.getName());
	}

	@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
	@Override
	public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
		super.onInitializeAccessibilityNodeInfo(info);
		info.setClassName(CalendarView.class.getName());
	}

	/**
	 * Gets the minimal date supported by this {@link CalendarView} in
	 * milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * <p>
	 * Note: The default minimal date is 01/01/1900.
	 * <p>
	 * 
	 * @return The minimal supported date.
	 * 
	 * @attr ref android.R.styleable#CalendarView_minDate
	 */
	public long getMinDate() {
		return mMinDate.getTimeInMillis();
	}

	/**
	 * Sets the minimal date supported by this {@link CalendarView} in
	 * milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * 
	 * @param minDate
	 *            The minimal supported date.
	 * 
	 * @attr ref android.R.styleable#CalendarView_minDate
	 */
	public void setMinDate(long minDate) {
		mTempDate.setTimeInMillis(minDate);
		if (isSameDate(mTempDate, mMinDate)) {
			return;
		}
		mMinDate.setTimeInMillis(minDate);
		// make sure the current date is not earlier than
		// the new min date since the latter is used for
		// calculating the indices in the adapter thus
		// avoiding out of bounds error
		Calendar date = mAdapter.mSelectedDate;
		if (date.before(mMinDate)) {
			mAdapter.setSelectedDay(mMinDate);
		}
		// reinitialize the adapter since its range depends on min date
		mAdapter.init();
		if (date.before(mMinDate)) {
			setDate(mTempDate.getTimeInMillis());
		} else {
			// we go to the current date to force the ListView to query its
			// adapter for the shown views since we have changed the adapter
			// range and the base from which the later calculates item indices
			// note that calling setDate will not work since the date is the
			// same
			goTo(date, false, true, false);
		}
	}

	/**
	 * Gets the maximal date supported by this {@link CalendarView} in
	 * milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * <p>
	 * Note: The default maximal date is 01/01/2100.
	 * <p>
	 * 
	 * @return The maximal supported date.
	 * 
	 * @attr ref android.R.styleable#CalendarView_maxDate
	 */
	public long getMaxDate() {
		return mMaxDate.getTimeInMillis();
	}

	/**
	 * Sets the maximal date supported by this {@link CalendarView} in
	 * milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * 
	 * @param maxDate
	 *            The maximal supported date.
	 * 
	 * @attr ref android.R.styleable#CalendarView_maxDate
	 */
	public void setMaxDate(long maxDate) {
		mTempDate.setTimeInMillis(maxDate);
		if (isSameDate(mTempDate, mMaxDate)) {
			return;
		}
		mMaxDate.setTimeInMillis(maxDate);
		// reinitialize the adapter since its range depends on max date
		mAdapter.init();
		Calendar date = mAdapter.mSelectedDate;
		if (date.after(mMaxDate)) {
			setDate(mMaxDate.getTimeInMillis());
		} else {
			// we go to the current date to force the ListView to query its
			// adapter for the shown views since we have changed the adapter
			// range and the base from which the later calculates item indices
			// note that calling setDate will not work since the date is the
			// same
			goTo(date, false, true, false);
		}
	}

	/**
	 * Sets whether to show the week number.
	 * 
	 * @param showWeekNumber
	 *            True to show the week number.
	 * 
	 * @attr ref android.R.styleable#CalendarView_showWeekNumber
	 */
	public void setShowWeekNumber(boolean showWeekNumber) {
		if (mShowWeekNumber == showWeekNumber) {
			return;
		}
		mShowWeekNumber = showWeekNumber;
		mAdapter.notifyDataSetChanged();
		setUpHeader();
	}

	/**
	 * Gets whether to show the week number.
	 * 
	 * @return True if showing the week number.
	 * 
	 * @attr ref android.R.styleable#CalendarView_showWeekNumber
	 */
	public boolean getShowWeekNumber() {
		return mShowWeekNumber;
	}

	/**
	 * Gets the first day of week.
	 * 
	 * @return The first day of the week conforming to the {@link CalendarView}
	 *         APIs.
	 * @see Calendar#MONDAY
	 * @see Calendar#TUESDAY
	 * @see Calendar#WEDNESDAY
	 * @see Calendar#THURSDAY
	 * @see Calendar#FRIDAY
	 * @see Calendar#SATURDAY
	 * @see Calendar#SUNDAY
	 * 
	 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
	 */
	public int getFirstDayOfWeek() {
		return mFirstDayOfWeek;
	}

	/**
	 * Sets the first day of week.
	 * 
	 * @param firstDayOfWeek
	 *            The first day of the week conforming to the
	 *            {@link CalendarView} APIs.
	 * @see Calendar#MONDAY
	 * @see Calendar#TUESDAY
	 * @see Calendar#WEDNESDAY
	 * @see Calendar#THURSDAY
	 * @see Calendar#FRIDAY
	 * @see Calendar#SATURDAY
	 * @see Calendar#SUNDAY
	 * 
	 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
	 */
	public void setFirstDayOfWeek(int firstDayOfWeek) {
		if (mFirstDayOfWeek == firstDayOfWeek) {
			return;
		}
		mFirstDayOfWeek = firstDayOfWeek;
		mAdapter.init();
		mAdapter.notifyDataSetChanged();
		setUpHeader();
	}

	/**
	 * Sets the listener to be notified upon selected date change.
	 * 
	 * @param listener
	 *            The listener to be notified.
	 */
	public void setOnDateChangeListener(OnDateChangeListener listener) {
		mOnDateChangeListener = listener;
	}

	/**
	 * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * 
	 * @return The selected date.
	 */
	public long getDate() {
		return mAdapter.mSelectedDate.getTimeInMillis();
	}

	/**
	 * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * 
	 * @param date
	 *            The selected date.
	 * 
	 * @throws IllegalArgumentException
	 *             of the provided date is before the minimal or after the
	 *             maximal date.
	 * 
	 * @see #setDate(long, boolean, boolean)
	 * @see #setMinDate(long)
	 * @see #setMaxDate(long)
	 */
	public void setDate(long date) {
		setDate(date, false, false);
	}

	/**
	 * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
	 * {@link TimeZone#getDefault()} time zone.
	 * 
	 * @param date
	 *            The date.
	 * @param animate
	 *            Whether to animate the scroll to the current date.
	 * @param center
	 *            Whether to center the current date even if it is already
	 *            visible.
	 * 
	 * @throws IllegalArgumentException
	 *             of the provided date is before the minimal or after the
	 *             maximal date.
	 * 
	 * @see #setMinDate(long)
	 * @see #setMaxDate(long)
	 */
	public void setDate(long date, boolean animate, boolean center) {
		mTempDate.setTimeInMillis(date);
		if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
			return;
		}
		goTo(mTempDate, animate, true, center);
	}

	private void updateDateTextSize() {
		TypedArray dateTextAppearance = getContext().obtainStyledAttributes(
				mDateTextAppearanceResId,
				R.styleable.TextAppearanceCompatStyleable);
		mDateTextSize = dateTextAppearance.getDimensionPixelSize(
				R.styleable.TextAppearanceCompatStyleable_android_textSize,
				DEFAULT_DATE_TEXT_SIZE);
		dateTextAppearance.recycle();
	}

	/**
	 * Invalidates all week views.
	 */
	private void invalidateAllWeekViews() {
		final int childCount = mListView.getChildCount();
		for (int i = 0; i < childCount; i++) {
			View view = mListView.getChildAt(i);
			view.invalidate();
		}
	}

	/**
	 * Sets the current locale.
	 * 
	 * @param locale
	 *            The current locale.
	 */
	private void setCurrentLocale(Locale locale) {
		if (locale.equals(mCurrentLocale)) {
			return;
		}

		mCurrentLocale = locale;

		mTempDate = getCalendarForLocale(mTempDate, locale);
		mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
		mMinDate = getCalendarForLocale(mMinDate, locale);
		mMaxDate = getCalendarForLocale(mMaxDate, locale);
	}

	/**
	 * Gets a calendar for locale bootstrapped with the value of a given
	 * calendar.
	 * 
	 * @param oldCalendar
	 *            The old calendar.
	 * @param locale
	 *            The locale.
	 */
	private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
		if (oldCalendar == null) {
			return Calendar.getInstance(locale);
		} else {
			final long currentTimeMillis = oldCalendar.getTimeInMillis();
			Calendar newCalendar = Calendar.getInstance(locale);
			newCalendar.setTimeInMillis(currentTimeMillis);
			return newCalendar;
		}
	}

	/**
	 * @return True if the <code>firstDate</code> is the same as the <code>
	 * secondDate</code>.
	 */
	private boolean isSameDate(Calendar firstDate, Calendar secondDate) {
		return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate
				.get(Calendar.DAY_OF_YEAR) && firstDate.get(Calendar.YEAR) == secondDate
				.get(Calendar.YEAR));
	}

	/**
	 * Creates a new adapter if necessary and sets up its parameters.
	 */
	private void setUpAdapter() {
		if (mAdapter == null) {
			mAdapter = new WeeksAdapter(getContext());
			mAdapter.registerDataSetObserver(new DataSetObserver() {
				@Override
				public void onChanged() {
					if (mOnDateChangeListener != null) {
						Calendar selectedDay = mAdapter.getSelectedDay();
						mOnDateChangeListener.onSelectedDayChange(
								CalendarView.this,
								selectedDay.get(Calendar.YEAR),
								selectedDay.get(Calendar.MONTH),
								selectedDay.get(Calendar.DAY_OF_MONTH));
					}
				}
			});
			mListView.setAdapter(mAdapter);
		}

		// refresh the view with the new parameters
		mAdapter.notifyDataSetChanged();
	}

	/**
	 * Sets up the strings to be used by the header.
	 */
	private void setUpHeader() {
		mDayLabels = new String[mDaysPerWeek];
		for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
			int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY
					: i;
			mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(
					calendarDay, DateUtils.LENGTH_SHORTEST);
		}

		TextView label = (TextView) mDayNamesHeader.getChildAt(0);
		if (mShowWeekNumber) {
			label.setVisibility(View.VISIBLE);
		} else {
			label.setVisibility(View.GONE);
		}
		for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
			label = (TextView) mDayNamesHeader.getChildAt(i);
			if (mWeekDayTextAppearanceResId > -1) {
				label.setTextAppearance(getContext(),
						mWeekDayTextAppearanceResId);
			}
			if (i < mDaysPerWeek + 1) {
				label.setText(mDayLabels[i - 1]);
				label.setVisibility(View.VISIBLE);
			} else {
				label.setVisibility(View.GONE);
			}
		}
		mDayNamesHeader.invalidate();
	}

	/**
	 * Sets all the required fields for the list view.
	 */
	@TargetApi(Build.VERSION_CODES.HONEYCOMB)
	private void setUpListView() {
		// Configure the listview
		mListView.setDivider(null);
		mListView.setItemsCanFocus(true);
		mListView.setVerticalScrollBarEnabled(false);
		mListView.setOnScrollListener(new OnScrollListener() {
			public void onScrollStateChanged(AbsListView view, int scrollState) {
				CalendarView.this.onScrollStateChanged(view, scrollState);
			}

			public void onScroll(AbsListView view, int firstVisibleItem,
					int visibleItemCount, int totalItemCount) {
				CalendarView.this.onScroll(view, firstVisibleItem,
						visibleItemCount, totalItemCount);
			}
		});
		// Make the scrolling behavior nicer
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
			mListView.setFriction(mFriction);
			mListView.setVelocityScale(mVelocityScale);
		}
	}

	/**
	 * This moves to the specified time in the view. If the time is not already
	 * in range it will move the list so that the first of the month containing
	 * the time is at the top of the view. If the new time is already in view
	 * the list will not be scrolled unless forceScroll is true. This time may
	 * optionally be highlighted as selected as well.
	 * 
	 * @param date
	 *            The time to move to.
	 * @param animate
	 *            Whether to scroll to the given time or just redraw at the new
	 *            location.
	 * @param setSelected
	 *            Whether to set the given time as selected.
	 * @param forceScroll
	 *            Whether to recenter even if the time is already visible.
	 * 
	 * @throws IllegalArgumentException
	 *             of the provided date is before the range start of after the
	 *             range end.
	 */
	@TargetApi(Build.VERSION_CODES.HONEYCOMB)
	private void goTo(Calendar date, boolean animate, boolean setSelected,
			boolean forceScroll) {
		if (date.before(mMinDate) || date.after(mMaxDate)) {
			throw new IllegalArgumentException("Time not between "
					+ mMinDate.getTime() + " and " + mMaxDate.getTime());
		}
		// Find the first and last entirely visible weeks
		int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
		View firstChild = mListView.getChildAt(0);
		if (firstChild != null && firstChild.getTop() < 0) {
			firstFullyVisiblePosition++;
		}
		int lastFullyVisiblePosition = firstFullyVisiblePosition
				+ mShownWeekCount - 1;
		if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
			lastFullyVisiblePosition--;
		}
		if (setSelected) {
			mAdapter.setSelectedDay(date);
		}
		// Get the week we're going to
		int position = getWeeksSinceMinDate(date);

		// Check if the selected day is now outside of our visible range
		// and if so scroll to the month that contains it
		if (position < firstFullyVisiblePosition
				|| position > lastFullyVisiblePosition || forceScroll) {
			mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
			mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);

			setMonthDisplayed(mFirstDayOfMonth);

			// the earliest time we can scroll to is the min date
			if (mFirstDayOfMonth.before(mMinDate)) {
				position = 0;
			} else {
				position = getWeeksSinceMinDate(mFirstDayOfMonth);
			}

			mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
			if (animate) {
				mListView.smoothScrollToPositionFromTop(position,
						mListScrollTopOffset, GOTO_SCROLL_DURATION);
			} else {
				mListView.setSelectionFromTop(position, mListScrollTopOffset);
				// Perform any after scroll operations that are needed
				onScrollStateChanged(mListView,
						OnScrollListener.SCROLL_STATE_IDLE);
			}
		} else if (setSelected) {
			// Otherwise just set the selection
			setMonthDisplayed(date);
		}
	}

	/**
	 * Parses the given <code>date</code> and in case of success sets the result
	 * to the <code>outDate</code>.
	 * 
	 * @return True if the date was parsed.
	 */
	private boolean parseDate(String date, Calendar outDate) {
		try {
			outDate.setTime(mDateFormat.parse(date));
			return true;
		} catch (ParseException e) {
			Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
			return false;
		}
	}

	/**
	 * Called when a <code>view</code> transitions to a new <code>scrollState
	 * </code>.
	 */
	private void onScrollStateChanged(AbsListView view, int scrollState) {
		mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
	}

	/**
	 * Updates the title and selected month if the <code>view</code> has moved
	 * to a new month.
	 */
	private void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		WeekView child = (WeekView) view.getChildAt(0);
		if (child == null) {
			return;
		}

		// Figure out where we are
		long currScroll = view.getFirstVisiblePosition() * child.getHeight()
				- child.getBottom();

		// If we have moved since our last call update the direction
		if (currScroll < mPreviousScrollPosition) {
			mIsScrollingUp = true;
		} else if (currScroll > mPreviousScrollPosition) {
			mIsScrollingUp = false;
		} else {
			return;
		}

		// Use some hysteresis for checking which month to highlight. This
		// causes the month to transition when two full weeks of a month are
		// visible when scrolling up, and when the first day in a month reaches
		// the top of the screen when scrolling down.
		int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
		if (mIsScrollingUp) {
			child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
		} else if (offset != 0) {
			child = (WeekView) view.getChildAt(offset);
		}

		// Find out which month we're moving into
		int month;
		if (mIsScrollingUp) {
			month = child.getMonthOfFirstWeekDay();
		} else {
			month = child.getMonthOfLastWeekDay();
		}

		// And how it relates to our current highlighted month
		int monthDiff;
		if (mCurrentMonthDisplayed == 11 && month == 0) {
			monthDiff = 1;
		} else if (mCurrentMonthDisplayed == 0 && month == 11) {
			monthDiff = -1;
		} else {
			monthDiff = month - mCurrentMonthDisplayed;
		}

		// Only switch months if we're scrolling away from the currently
		// selected month
		if ((!mIsScrollingUp && monthDiff > 0)
				|| (mIsScrollingUp && monthDiff < 0)) {
			Calendar firstDay = child.getFirstDay();
			if (mIsScrollingUp) {
				firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
			} else {
				firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
			}
			setMonthDisplayed(firstDay);
		}
		mPreviousScrollPosition = currScroll;
		mPreviousScrollState = mCurrentScrollState;
	}

	/**
	 * Sets the month displayed at the top of this view based on time. Override
	 * to add custom events when the title is changed.
	 * 
	 * @param calendar
	 *            A day in the new focus month.
	 */
	private void setMonthDisplayed(Calendar calendar) {
		final int newMonthDisplayed = calendar.get(Calendar.MONTH);
		if (mCurrentMonthDisplayed != newMonthDisplayed) {
			mCurrentMonthDisplayed = newMonthDisplayed;
			mAdapter.setFocusMonth(mCurrentMonthDisplayed);
			final int flags = DateUtils.FORMAT_SHOW_DATE
					| DateUtils.FORMAT_NO_MONTH_DAY
					| DateUtils.FORMAT_SHOW_YEAR;
			final long millis = calendar.getTimeInMillis();
			String newMonthName = DateUtils.formatDateRange(getContext(),
					millis, millis, flags);
			mMonthName.setText(newMonthName);
			mMonthName.invalidate();
		}
	}

	/**
	 * @return Returns the number of weeks between the current <code>date</code>
	 *         and the <code>mMinDate</code>.
	 */
	private int getWeeksSinceMinDate(Calendar date) {
		if (date.before(mMinDate)) {
			throw new IllegalArgumentException("fromDate: "
					+ mMinDate.getTime() + " does not precede toDate: "
					+ date.getTime());
		}
		long endTimeMillis = date.getTimeInMillis()
				+ date.getTimeZone().getOffset(date.getTimeInMillis());
		long startTimeMillis = mMinDate.getTimeInMillis()
				+ mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
		long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
				* MILLIS_IN_DAY;
		return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
	}

	/**
	 * Command responsible for acting upon scroll state changes.
	 */
	private class ScrollStateRunnable implements Runnable {
		private AbsListView mView;

		private int mNewState;

		/**
		 * Sets up the runnable with a short delay in case the scroll state
		 * immediately changes again.
		 * 
		 * @param view
		 *            The list view that changed state
		 * @param scrollState
		 *            The new state it changed to
		 */
		public void doScrollStateChange(AbsListView view, int scrollState) {
			mView = view;
			mNewState = scrollState;
			removeCallbacks(this);
			postDelayed(this, SCROLL_CHANGE_DELAY);
		}

		public void run() {
			mCurrentScrollState = mNewState;
			// Fix the position after a scroll or a fling ends
			if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
					&& mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
				View child = mView.getChildAt(0);
				if (child == null) {
					// The view is no longer visible, just return
					return;
				}
				int dist = child.getBottom() - mListScrollTopOffset;
				if (dist > mListScrollTopOffset) {
					if (mIsScrollingUp) {
						mView.smoothScrollBy(dist - child.getHeight(),
								ADJUSTMENT_SCROLL_DURATION);
					} else {
						mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
					}
				}
			}
			mPreviousScrollState = mNewState;
		}
	}

	/**
	 * <p>
	 * This is a specialized adapter for creating a list of weeks with
	 * selectable days. It can be configured to display the week number, start
	 * the week on a given day, show a reduced number of days, or display an
	 * arbitrary number of weeks at a time.
	 * </p>
	 */
	private class WeeksAdapter extends BaseAdapter implements OnTouchListener {

		private int mSelectedWeek;

		private GestureDetector mGestureDetector;

		private int mFocusedMonth;

		private final Calendar mSelectedDate = Calendar.getInstance();

		private int mTotalWeekCount;

		public WeeksAdapter(Context context) {
			mGestureDetector = new GestureDetector(getContext(),
					new CalendarGestureListener());
			init();
		}

		/**
		 * Set up the gesture detector and selected time
		 */
		private void init() {
			mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
			mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
			if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
					|| mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
				mTotalWeekCount++;
			}
		}

		/**
		 * Updates the selected day and related parameters.
		 * 
		 * @param selectedDay
		 *            The time to highlight
		 */
		public void setSelectedDay(Calendar selectedDay) {
			if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate
					.get(Calendar.DAY_OF_YEAR)
					&& selectedDay.get(Calendar.YEAR) == mSelectedDate
							.get(Calendar.YEAR)) {
				return;
			}
			mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
			mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
			mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
			notifyDataSetChanged();
		}

		/**
		 * @return The selected day of month.
		 */
		public Calendar getSelectedDay() {
			return mSelectedDate;
		}

		@Override
		public int getCount() {
			return mTotalWeekCount;
		}

		@Override
		public Object getItem(int position) {
			return null;
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			WeekView weekView = null;
			if (convertView != null) {
				weekView = (WeekView) convertView;
			} else {
				weekView = new WeekView(getContext());
				android.widget.AbsListView.LayoutParams params = new android.widget.AbsListView.LayoutParams(
						LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
				weekView.setLayoutParams(params);
				weekView.setClickable(true);
				weekView.setOnTouchListener(this);
			}

			int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate
					.get(Calendar.DAY_OF_WEEK) : -1;
			weekView.init(position, selectedWeekDay, mFocusedMonth);

			return weekView;
		}

		/**
		 * Changes which month is in focus and updates the view.
		 * 
		 * @param month
		 *            The month to show as in focus [0-11]
		 */
		public void setFocusMonth(int month) {
			if (mFocusedMonth == month) {
				return;
			}
			mFocusedMonth = month;
			notifyDataSetChanged();
		}

		@Override
		public boolean onTouch(View v, MotionEvent event) {
			if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
				WeekView weekView = (WeekView) v;
				// if we cannot find a day for the given location we are done
				if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
					return true;
				}
				// it is possible that the touched day is outside the valid
				// range
				// we draw whole weeks but range end can fall not on the week
				// end
				if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
					return true;
				}
				onDateTapped(mTempDate);
				return true;
			}
			return false;
		}

		/**
		 * Maintains the same hour/min/sec but moves the day to the tapped day.
		 * 
		 * @param day
		 *            The day that was tapped
		 */
		private void onDateTapped(Calendar day) {
			setSelectedDay(day);
			setMonthDisplayed(day);
		}

		/**
		 * This is here so we can identify single tap events and set the
		 * selected day correctly
		 */
		class CalendarGestureListener extends
				GestureDetector.SimpleOnGestureListener {
			@Override
			public boolean onSingleTapUp(MotionEvent e) {
				return true;
			}
		}
	}

	/**
	 * <p>
	 * This is a dynamic view for drawing a single week. It can be configured to
	 * display the week number, start the week on a given day, or show a reduced
	 * number of days. It is intended for use as a single view within a
	 * ListView. See {@link WeeksAdapter} for usage.
	 * </p>
	 */
	private class WeekView extends View {

		private final Rect mTempRect = new Rect();

		private final Paint mDrawPaint = new Paint();

		private final Paint mMonthNumDrawPaint = new Paint();

		// Cache the number strings so we don't have to recompute them each time
		private String[] mDayNumbers;

		// Quick lookup for checking which days are in the focus month
		private boolean[] mFocusDay;

		// Whether this view has a focused day.
		private boolean mHasFocusedDay;

		// Whether this view has only focused days.
		private boolean mHasUnfocusedDay;

		// The first day displayed by this item
		private Calendar mFirstDay;

		// The month of the first day in this week
		private int mMonthOfFirstWeekDay = -1;

		// The month of the last day in this week
		private int mLastWeekDayMonth = -1;

		// The position of this week, equivalent to weeks since the week of Jan
		// 1st, 1900
		private int mWeek = -1;

		// Quick reference to the width of this view, matches parent
		private int mWidth;

		// The height this view should draw at in pixels, set by height param
		private int mHeight;

		// If this view contains the selected day
		private boolean mHasSelectedDay = false;

		// Which day is selected [0-6] or -1 if no day is selected
		private int mSelectedDay = -1;

		// The number of days + a spot for week number if it is displayed
		private int mNumCells;

		// The left edge of the selected day
		private int mSelectedLeft = -1;

		// The right edge of the selected day
		private int mSelectedRight = -1;

		public WeekView(Context context) {
			super(context);

			// Sets up any standard paints that will be used
			initilaizePaints();
		}

		/**
		 * Initializes this week view.
		 * 
		 * @param weekNumber
		 *            The number of the week this view represents. The week
		 *            number is a zero based index of the weeks since
		 *            {@link CalendarView#getMinDate()}.
		 * @param selectedWeekDay
		 *            The selected day of the week from 0 to 6, -1 if no
		 *            selected day.
		 * @param focusedMonth
		 *            The month that is currently in focus i.e. highlighted.
		 */
		public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
			mSelectedDay = selectedWeekDay;
			mHasSelectedDay = mSelectedDay != -1;
			mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
			mWeek = weekNumber;
			mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());

			mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
			mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);

			// Allocate space for caching the day numbers and focus values
			mDayNumbers = new String[mNumCells];
			mFocusDay = new boolean[mNumCells];

			// If we're showing the week number calculate it based on Monday
			int i = 0;
			if (mShowWeekNumber) {
				mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
						mTempDate.get(Calendar.WEEK_OF_YEAR));
				i++;
			}

			// Now adjust our starting day based on the start day of the week
			int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
			mTempDate.add(Calendar.DAY_OF_MONTH, diff);

			mFirstDay = (Calendar) mTempDate.clone();
			mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);

			mHasUnfocusedDay = true;
			for (; i < mNumCells; i++) {
				final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
				mFocusDay[i] = isFocusedDay;
				mHasFocusedDay |= isFocusedDay;
				mHasUnfocusedDay &= !isFocusedDay;
				// do not draw dates outside the valid range to avoid user
				// confusion
				if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
					mDayNumbers[i] = "";
				} else {
					mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
							mTempDate.get(Calendar.DAY_OF_MONTH));
				}
				mTempDate.add(Calendar.DAY_OF_MONTH, 1);
			}
			// We do one extra add at the end of the loop, if that pushed us to
			// new month undo it
			if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
				mTempDate.add(Calendar.DAY_OF_MONTH, -1);
			}
			mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);

			updateSelectionPositions();
		}

		/**
		 * Initialize the paint instances.
		 */
		private void initilaizePaints() {
			mDrawPaint.setFakeBoldText(false);
			mDrawPaint.setAntiAlias(true);
			mDrawPaint.setStyle(Style.FILL);

			mMonthNumDrawPaint.setFakeBoldText(true);
			mMonthNumDrawPaint.setAntiAlias(true);
			mMonthNumDrawPaint.setStyle(Style.FILL);
			mMonthNumDrawPaint.setTextAlign(Align.CENTER);
			mMonthNumDrawPaint.setTextSize(mDateTextSize);
		}

		/**
		 * Returns the month of the first day in this week.
		 * 
		 * @return The month the first day of this view is in.
		 */
		public int getMonthOfFirstWeekDay() {
			return mMonthOfFirstWeekDay;
		}

		/**
		 * Returns the month of the last day in this week
		 * 
		 * @return The month the last day of this view is in
		 */
		public int getMonthOfLastWeekDay() {
			return mLastWeekDayMonth;
		}

		/**
		 * Returns the first day in this view.
		 * 
		 * @return The first day in the view.
		 */
		public Calendar getFirstDay() {
			return mFirstDay;
		}

		/**
		 * Calculates the day that the given x position is in, accounting for
		 * week number.
		 * 
		 * @param x
		 *            The x position of the touch event.
		 * @return True if a day was found for the given location.
		 */
		public boolean getDayFromLocation(float x, Calendar outCalendar) {
			final boolean isLayoutRtl = isLayoutRtl();

			int start;
			int end;

			if (isLayoutRtl) {
				start = 0;
				end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
			} else {
				start = mShowWeekNumber ? mWidth / mNumCells : 0;
				end = mWidth;
			}

			if (x < start || x > end) {
				outCalendar.clear();
				return false;
			}

			// Selection is (x - start) / (pixels/day) which is (x - start) *
			// day / pixels
			int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));

			if (isLayoutRtl) {
				dayPosition = mDaysPerWeek - 1 - dayPosition;
			}

			outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
			outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);

			return true;
		}

		@Override
		protected void onDraw(Canvas canvas) {
			drawBoder(canvas);
			drawBackground(canvas);
			drawSelectedDayBackgroud(canvas);
			drawWeekNumbersAndDates(canvas);
			drawWeekSeparators(canvas);
			drawDaySeparators(canvas);
			drawSelectedDateVerticalBars(canvas);
		}

		private void drawBoder(Canvas canvas) {
			
			Style oldStyle = mDrawPaint.getStyle();
			mDrawPaint.setColor(mBoderColor);
			mDrawPaint.setStyle(Style.STROKE);
			mTempRect.top = 0;
			mTempRect.bottom = mHeight;
			mTempRect.left = 0;
			mTempRect.right = mWidth;
			canvas.drawRect(mTempRect, mDrawPaint);
			mDrawPaint.setStyle(oldStyle);
		}

		/**
		 * This draws the selection highlight if a day is selected in this week.
		 * 
		 * @param canvas
		 *            The canvas to draw on
		 */
		private void drawBackground(Canvas canvas) {
			if (!mHasSelectedDay) {
				return;
			}
			mDrawPaint.setColor(mSelectedWeekBackgroundColor);

			mTempRect.top = mWeekSeperatorLineWidth;
			mTempRect.bottom = mHeight;

			final boolean isLayoutRtl = isLayoutRtl();

			if (isLayoutRtl) {
				mTempRect.left = 0;
				mTempRect.right = mSelectedLeft - 2;
			} else {
				mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
				mTempRect.right = mSelectedLeft - 2;
			}
			canvas.drawRect(mTempRect, mDrawPaint);

			if (isLayoutRtl) {
				mTempRect.left = mSelectedRight + 3;
				mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells
						: mWidth;
			} else {
				mTempRect.left = mSelectedRight + 3;
				mTempRect.right = mWidth;
			}
			canvas.drawRect(mTempRect, mDrawPaint);
		}

		/**
		 * Draws the week and month day numbers for this week.
		 * 
		 * @param canvas
		 *            The canvas to draw on
		 */
		private void drawWeekNumbersAndDates(Canvas canvas) {
			final float textHeight = mDrawPaint.getTextSize();
			final int y = (int) ((mHeight + textHeight) / 2)
					- mWeekSeperatorLineWidth;
			final int nDays = mNumCells;
			final int divisor = 2 * nDays;

			mDrawPaint.setTextAlign(Align.CENTER);
			mDrawPaint.setTextSize(mDateTextSize);

			int i = 0;

			if (isLayoutRtl()) {
				for (; i < nDays - 1; i++) {
					mMonthNumDrawPaint
							.setColor(mFocusDay[i] ? mFocusedMonthDateColor
									: mUnfocusedMonthDateColor);
					int x = (2 * i + 1) * mWidth / divisor;
					canvas.drawText(mDayNumbers[nDays - 1 - i], x, y,
							mMonthNumDrawPaint);
				}
				if (mShowWeekNumber) {
					mDrawPaint.setColor(mWeekNumberColor);
					int x = mWidth - mWidth / divisor;
					canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
				}
			} else {
				if (mShowWeekNumber) {
					mDrawPaint.setColor(mWeekNumberColor);
					int x = mWidth / divisor;
					canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
					i++;
				}
				for (; i < nDays; i++) {
					mMonthNumDrawPaint
							.setColor(mFocusDay[i] ? mFocusedMonthDateColor
									: mUnfocusedMonthDateColor);
					int x = (2 * i + 1) * mWidth / divisor;
					canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
				}
			}
		}

		/**
		 * Draws a horizontal line for separating the weeks.
		 * 
		 * @param canvas
		 *            The canvas to draw on.
		 */
		private void drawWeekSeparators(Canvas canvas) {
			// If it is the topmost fully visible child do not draw separator
			// line
			int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
			if (mListView.getChildAt(0).getTop() < 0) {
				firstFullyVisiblePosition++;
			}
			if (firstFullyVisiblePosition == mWeek) {
				return;
			}
			mDrawPaint.setColor(mWeekSeparatorLineColor);
			mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth);
			float startX;
			float stopX;
			if (isLayoutRtl()) {
				startX = 0;
				stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
			} else {
				startX = mShowWeekNumber ? mWidth / mNumCells : 0;
				stopX = mWidth;
			}
			canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
		}

		private void drawDaySeparators(Canvas canvas) {
			
			final int nDays = mNumCells;

			mDrawPaint.setColor(mDaySeparatorLineColor);

			for (int i = 1; i < nDays; i++) {
				int x = i * mWidth / nDays;
				canvas.drawLine(x, 1, x, mHeight - 1, mDrawPaint);
			}
		}

		/**
		 * Draws the selected date bars if this week has a selected day.
		 * 
		 * @param canvas
		 *            The canvas to draw on
		 */
		private void drawSelectedDateVerticalBars(Canvas canvas) {
			if (!mHasSelectedDay) {
				return;
			}
			mSelectedDateVerticalBar.setBounds(mSelectedLeft
					- mSelectedDateVerticalBarWidth / 2,
					mWeekSeperatorLineWidth, mSelectedLeft
							+ mSelectedDateVerticalBarWidth / 2, mHeight);
			mSelectedDateVerticalBar.draw(canvas);
			mSelectedDateVerticalBar.setBounds(mSelectedRight
					- mSelectedDateVerticalBarWidth / 2,
					mWeekSeperatorLineWidth, mSelectedRight
							+ mSelectedDateVerticalBarWidth / 2, mHeight);
			mSelectedDateVerticalBar.draw(canvas);
		}

		private void drawSelectedDayBackgroud(Canvas canvas) {
			if (!mHasSelectedDay) {
				return;
			}
			Rect rect = new Rect(mSelectedLeft, mWeekSeperatorLineWidth,
					mSelectedRight, mHeight);
			
			Style oldStyle = mDrawPaint.getStyle();
			mDrawPaint.setColor(mSelectedDayBackgroundFillColor);
			mDrawPaint.setStyle(Style.FILL);
			canvas.drawRect(rect, mDrawPaint);
			mDrawPaint.setColor(mSelectedDayBackgroundBoderColor);
			mDrawPaint.setStyle(Style.STROKE);
			canvas.drawRect(rect, mDrawPaint);
			mDrawPaint.setStyle(oldStyle);
		}

		@Override
		protected void onSizeChanged(int w, int h, int oldw, int oldh) {
			mWidth = w;
			updateSelectionPositions();
		}

		/**
		 * This calculates the positions for the selected day lines.
		 */
		private void updateSelectionPositions() {
			if (mHasSelectedDay) {
				final boolean isLayoutRtl = isLayoutRtl();
				int selectedPosition = mSelectedDay - mFirstDayOfWeek;
				if (selectedPosition < 0) {
					selectedPosition += 7;
				}
				if (mShowWeekNumber && !isLayoutRtl) {
					selectedPosition++;
				}
				if (isLayoutRtl) {
					mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition)
							* mWidth / mNumCells;

				} else {
					mSelectedLeft = selectedPosition * mWidth / mNumCells;
				}
				mSelectedRight = mSelectedLeft + mWidth / mNumCells;
			}
		}

		@Override
		protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
			mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
					.getPaddingBottom()) / mShownWeekCount;
			setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
		}
	}

	private boolean isLayoutRtl() {
		// No API17 in maven central..
		// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
		// return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
		// }

		return false;
	}
}
